{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2fef9997",
   "metadata": {},
   "outputs": [],
   "source": [
    "# -*- coding: utf-8 -*-\n",
    "import sys\n",
    "from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QPushButton, QFileDialog, QVBoxLayout, QHBoxLayout, QMessageBox\n",
    "from PyQt5.QtGui import QPixmap, QFont, QPalette, QBrush, QImage\n",
    "from PyQt5.QtCore import Qt, QTimer\n",
    "import torch\n",
    "import torch.nn as nn\n",
    "import torchvision.models as models\n",
    "from PIL import Image\n",
    "import torchvision.transforms as transforms\n",
    "import os\n",
    "import cv2\n",
    "import numpy as np\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2e43d2c9",
   "metadata": {},
   "source": [
    "编码声明：# -*- coding: utf-8 -*-  \n",
    "导入所需要的模块，为后续实现一个结合PyQt5 GUI和深度学习模型的水果识别应用程序奠定了基础。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ed27d7cf",
   "metadata": {},
   "outputs": [],
   "source": [
    "\n",
    "# 定义资源路径函数\n",
    "def resource_path(relative_path):\n",
    "    \"\"\"获取打包后的资源路径\"\"\"\n",
    "    if hasattr(sys, '_MEIPASS'):\n",
    "        return os.path.join(sys._MEIPASS, relative_path)\n",
    "    return os.path.join(os.path.abspath(\".\"), relative_path)\n",
    "\n",
    "classes = ('粑粑柑', '白兰瓜', '白萝卜', '白心火龙果', '百香果', '菠萝', '菠萝莓', '菠萝蜜', '草莓', '车厘子', '番石榴-百', '番石榴-红', '佛手瓜', ' 甘蔗', '桂圆', '哈密瓜', '黑莓', '红苹果', '红心火龙果', '胡萝卜', '黄桃', '金桔', '橘子', '蓝莓', '梨', '李子', '荔枝', '莲雾', '榴莲', ' 芦柑', '芒果', '毛丹', '猕猴桃', '木瓜', '柠檬', '牛油果', '蟠桃', '枇杷', '葡萄-白', '葡萄-红', '脐橙', '青柠', '青苹果', '人参果', '桑葚', '沙果', '沙棘', '砂糖橘', '山楂', '山竹', '蛇皮果', '圣女果', '石榴', '柿子', '树莓', '水蜜桃', '酸角', '甜瓜-白', '甜瓜-金', '甜瓜-绿', '甜瓜-伊丽莎白', '沃柑', '无花果', '西瓜', '西红柿', '西梅', '西柚', '香蕉', '香橼', '杏', '血橙', '羊角蜜', '羊奶果', '杨梅', '杨桃', '腰 果', '椰子', '樱桃', '油桃', '柚子', '枣')\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f72f92fc",
   "metadata": {},
   "source": [
    "主要定义了一个资源路径函数 resource_path 和一个包含水果类别的元组 classes。  \n",
    "resource_path 函数：  \n",
    "\n",
    "用于在开发环境和打包环境中正确加载资源文件（如图像、模型文件等）。  \n",
    "确保应用程序在不同环境下都能正常运行。  \n",
    "\n",
    "classes 元组：  \n",
    "定义了水果识别的类别列表，用于深度学习模型的输出层。  \n",
    "为后续的水果识别任务提供了明确的类别标签。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "26094243",
   "metadata": {},
   "outputs": [],
   "source": [
    "\n",
    "transform_test = transforms.Compose([\n",
    "    transforms.Resize((224, 224)),\n",
    "    transforms.ToTensor(),\n",
    "    transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5])\n",
    "])\n",
    "\n",
    "DEVICE = torch.device(\"cpu\")\n",
    "# model = models.vgg16(weights=None)\n",
    "# num_ftrs = model.classifier[6].in_features\n",
    "# model.classifier[6] = nn.Linear(num_ftrs, len(classes))\n",
    "\n",
    "# 改成外部模型路径\n",
    "model_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), \"model.pth\")\n",
    "if not os.path.exists(model_path):\n",
    "    raise FileNotFoundError(f\"The model file {model_path} does not exist.\")\n",
    "\n",
    "model = models.vgg16(weights=None)\n",
    "num_ftrs = model.classifier[6].in_features\n",
    "model.classifier[6] = nn.Linear(num_ftrs, len(classes))\n",
    "model.load_state_dict(torch.load(model_path, map_location=DEVICE))\n",
    "model.to(DEVICE)\n",
    "model.eval()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6dd04cea",
   "metadata": {},
   "source": [
    "数据预处理设置：定义了一个名为transform_test的图像预处理流水线，包含图像缩放、张量转换和归一化操作，用于将输入图像转换为模型所需的格式。  \n",
    "设备配置：设置模型运行设备为 CPU，当然这里也可以调用GPU进行模型的推理，但考虑到后续打包的所占用的内存以及依赖的库，这里设置为使用CPU。    \n",
    "模型加载与修改：  \n",
    "初始化预训练的 VGG16 模型（不加载预训练权重）  \n",
    "修改最后一层全连接层，使其输出类别数适应具体任务  \n",
    "从本地路径加载训练好的模型权重  \n",
    "将模型移至指定设备并设置为评估模式  \n",
    "路径管理：通过 os 模块构建模型文件的绝对路径，并在文件不存在时抛出异常，模型文件的存放位置与打包文件在同一文件夹下。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e92b67b4",
   "metadata": {},
   "outputs": [],
   "source": [
    "\n",
    "class FruitRecognitionApp(QWidget):\n",
    "    def __init__(self):\n",
    "        super().__init__()\n",
    "        print(\"初始化函数开始\")\n",
    "        self.camera = cv2.VideoCapture(0)\n",
    "        self.timer = QTimer()\n",
    "        self.timer.timeout.connect(self.update_frame)\n",
    "        self.is_camera_on = False\n",
    "        self.initUI()\n",
    "        print(\"初始化UI完成\")\n",
    "\n",
    "    def initUI(self):\n",
    "        self.setWindowTitle('水果识别')\n",
    "        self.setGeometry(100, 100, 1200, 800)\n",
    "        palette = QPalette()\n",
    "\n",
    "        # 使用 resource_path 加载背景图片\n",
    "        # background_image_path = resource_path(\"Backdrop.jpg\")\n",
    "        # background_image = QPixmap(background_image_path)\n",
    "        # if not background_image.isNull():\n",
    "        #     palette.setBrush(QPalette.Background, QBrush(background_image))\n",
    "        palette.setColor(QPalette.Background, Qt.white)  # 使用白色背景\n",
    "        self.setPalette(palette)\n",
    "\n",
    "        # 左侧显示原始图片或视频\n",
    "        self.labelOriginal = QLabel(self)\n",
    "        self.labelOriginal.setAlignment(Qt.AlignCenter)\n",
    "        self.labelOriginal.setStyleSheet(\"border: 2px dashed black; padding: 10px; background-color: rgba(255, 255, 255, 150);\")\n",
    "\n",
    "        # 右侧显示结果图片\n",
    "        self.labelResult = QLabel(self)\n",
    "        self.labelResult.setAlignment(Qt.AlignCenter)\n",
    "        self.labelResult.setStyleSheet(\"border: 2px dashed black; padding: 10px; background-color: rgba(255, 255, 255, 150);\")\n",
    "\n",
    "        # 显示预测结果的标签\n",
    "        self.resultLabel = QLabel('预测类别: ')\n",
    "        self.resultLabel.setFont(QFont('Arial', 18))\n",
    "\n",
    "        # 按钮：上传图片、新图片上传、摄像头捕获\n",
    "        self.uploadButton = QPushButton('上传图片')\n",
    "        self.uploadButton.clicked.connect(self.uploadImage)\n",
    "        self.newUploadButton = QPushButton('上传新图片')\n",
    "        self.newUploadButton.clicked.connect(self.reset)\n",
    "        self.cameraButton = QPushButton('打开摄像头')\n",
    "        self.cameraButton.clicked.connect(self.toggle_camera)\n",
    "\n",
    "        # 布局\n",
    "        layout = QVBoxLayout()\n",
    "        layout.addWidget(self.labelOriginal)\n",
    "        layout.addWidget(self.labelResult)\n",
    "        layout.addWidget(self.resultLabel)\n",
    "        buttonLayout = QHBoxLayout()\n",
    "        buttonLayout.addWidget(self.uploadButton)\n",
    "        buttonLayout.addWidget(self.newUploadButton)\n",
    "        buttonLayout.addWidget(self.cameraButton)\n",
    "        layout.addLayout(buttonLayout)\n",
    "        self.setLayout(layout)\n",
    "\n",
    "        print(\"初始化界面\")\n",
    "        # 原有代码\n",
    "        self.setWindowTitle('水果识别')\n",
    "        # ...\n",
    "        print(\"界面设置完毕\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3c08a823",
   "metadata": {},
   "source": [
    "这段代码定义了一个名为FruitRecognitionApp的 PyQt5 应用程序类，用于实现水果识别功能的图形界面，主要功能包括：  \n",
    "\n",
    "初始化与资源管理：  \n",
    "创建摄像头捕获对象和定时器  \n",
    "管理摄像头状态标志  \n",
    "初始化用户界面  \n",
    "用户界面布局：  \n",
    "设置窗口标题、尺寸和背景  \n",
    "创建左右两个显示区域（原始图像和识别结果）  \n",
    "添加结果标签和功能按钮（上传图片、上传新图片、开关摄像头）  \n",
    "使用垂直和水平布局管理器组织界面元素  \n",
    "核心功能：  \n",
    "图片上传与识别  \n",
    "摄像头实时捕获与识别  \n",
    "识别结果展示  \n",
    "交互控制：  \n",
    "通过按钮实现图片上传、重置和摄像头开关控制  \n",
    "使用定时器实现摄像头画面的实时更新  \n",
    "状态管理：  \n",
    "记录摄像头开关状态  \n",
    "提供重置功能以清除当前图片和结果  "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5fc91489",
   "metadata": {},
   "outputs": [],
   "source": [
    "\n",
    "    def uploadImage(self):\n",
    "        options = QFileDialog.Options()\n",
    "        fileName, _ = QFileDialog.getOpenFileName(self, \"选择图片\", \"\", \"Images (*.png *.jpg *.jpeg)\", options=options)\n",
    "        if fileName:\n",
    "            self.displayImage(fileName)\n",
    "            self.newUploadButton.setEnabled(True)\n",
    "            # 直接调用识别逻辑\n",
    "            self.predictImage(fileName)\n",
    "\n",
    "    def reset(self):\n",
    "        self.labelOriginal.clear()\n",
    "        self.labelResult.clear()\n",
    "        self.resultLabel.setText('预测类别: ')\n",
    "        self.newUploadButton.setEnabled(False)\n",
    "\n",
    "    def displayImage(self, imagePath):\n",
    "        self.labelOriginal.setPixmap(QPixmap(imagePath).scaled(400, 400, Qt.KeepAspectRatio))\n",
    "\n",
    "    def predictImage(self, image):\n",
    "        try:\n",
    "            print(\"开始预测\")\n",
    "            if isinstance(image, str):  # 如果是文件路径\n",
    "                print(f\"加载图片路径：{image}\")\n",
    "                image = Image.open(image)\n",
    "            elif isinstance(image, QImage):  # 如果是 QImage 对象\n",
    "                print(\"处理QImage对象\")\n",
    "                image = self.qimage_to_pil(image)\n",
    "            else:\n",
    "                print(\"未知图片类型\")\n",
    "            print(f\"预处理前图像：{image}\")\n",
    "            # 图像预处理\n",
    "            image_tensor = transform_test(image).unsqueeze(0).to(DEVICE)\n",
    "            print(f\"处理后tensor: {image_tensor.shape}\")\n",
    "            # 模型推理\n",
    "            with torch.no_grad():\n",
    "                outputs = model(image_tensor)\n",
    "                print(f\"模型输出: {outputs}\")\n",
    "                _, predicted = torch.max(outputs, 1)\n",
    "                class_name = classes[predicted.item()]\n",
    "                print(f\"预测类别：{class_name}\")\n",
    "            self.resultLabel.setText(f'预测类别: {class_name}')\n",
    "            # 显示图片逻辑\n",
    "            # ...\n",
    "        except Exception as e:\n",
    "            print(\"预测出错:\", e)\n",
    "            import traceback; traceback.print_exc()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "97d1fcd2",
   "metadata": {},
   "source": [
    "这段代码实现了水果识别应用的核心功能模块，主要包括：  \n",
    "\n",
    "图片上传与显示：  \n",
    "uploadImage()：打开文件对话框选择图片，显示原始图像并触发预测  \n",
    "displayImage()：将图片按比例缩放至 400×400 像素并显示在界面上  \n",
    "reset()：清除当前显示的图片和预测结果  \n",
    "图像预测流程：  \n",
    "predictImage()：处理图片输入（支持文件路径或 QImage 对象）  \n",
    "预处理：使用transform_test转换图像为模型输入格式  \n",
    "模型推理：通过 VGG16 模型进行预测，获取类别索引  \n",
    "结果显示：将预测的水果类别名称更新到界面标签  \n",
    "异常处理：  \n",
    "捕获并打印预测过程中的异常信息  \n",
    "支持打印详细的堆栈跟踪  \n",
    "\n",
    "该模块通过统一的接口处理不同来源的图像输入，实现了从图像加载、预处理、模型推理到结果展示的完整流程，并提供了错误处理机制保证程序的健壮性。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "afa9e9c0",
   "metadata": {},
   "outputs": [],
   "source": [
    "\n",
    "    def qimage_to_pil(self, qImg):\n",
    "        \"\"\"将 QImage 转换为 PIL.Image\"\"\"\n",
    "        qImg = qImg.convertToFormat(QImage.Format_RGB888)\n",
    "        width = qImg.width()\n",
    "        height = qImg.height()\n",
    "        ptr = qImg.bits()\n",
    "        ptr.setsize(qImg.byteCount())\n",
    "        arr = np.array(ptr).reshape(height, width, 3)  # 转换为 numpy 数组\n",
    "        return Image.fromarray(arr)  # 转换为 PIL.Image\n",
    "\n",
    "    def toggle_camera(self):\n",
    "        \"\"\"切换摄像头状态\"\"\"\n",
    "        if self.is_camera_on:\n",
    "            self.timer.stop()  # 停止定时器\n",
    "            self.cameraButton.setText('打开摄像头')\n",
    "            self.is_camera_on = False\n",
    "        else:\n",
    "            self.timer.start(30)  # 每30毫秒更新一帧\n",
    "            self.cameraButton.setText('关闭摄像头')\n",
    "            self.is_camera_on = True\n",
    "\n",
    "    def update_frame(self):\n",
    "        ret, frame = self.camera.read()\n",
    "        if ret:\n",
    "            try:\n",
    "                print(\"捕获一帧\")\n",
    "                frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)\n",
    "                image_pil = Image.fromarray(frame_rgb)\n",
    "                # 预测\n",
    "                print(\"开始识别摄像头画面\")\n",
    "                with torch.no_grad():\n",
    "                    image_tensor = transform_test(image_pil).unsqueeze(0).to(DEVICE)\n",
    "                    outputs = model(image_tensor)\n",
    "                    _, predicted = torch.max(outputs, 1)\n",
    "                    class_name = classes[predicted.item()]\n",
    "                    print(f\"预测类别：{class_name}\")\n",
    "                # 绘制文字\n",
    "                frame_with_text = cv2.putText(\n",
    "                    frame_rgb,\n",
    "                    f\"Predicted: {class_name}\",\n",
    "                    (10, 30),\n",
    "                    cv2.FONT_HERSHEY_SIMPLEX,\n",
    "                    1,\n",
    "                    (0, 255, 0),\n",
    "                    2\n",
    "                )\n",
    "                height, width, channel = frame_with_text.shape\n",
    "                bytesPerLine = 3 * width\n",
    "                qImg = QImage(frame_with_text.data, width, height, bytesPerLine, QImage.Format_RGB888)\n",
    "                self.labelOriginal.setPixmap(QPixmap.fromImage(qImg).scaled(400, 400, Qt.KeepAspectRatio))\n",
    "                # 更新预测文本\n",
    "                self.resultLabel.setText(f'预测类别: {class_name}')\n",
    "            except Exception as e:\n",
    "                print(\"更新摄像头画面出错：\", e)\n",
    "                import traceback; traceback.print_exc()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "05588495",
   "metadata": {},
   "source": [
    "这段代码实现了水果识别应用的摄像头实时识别功能，主要包括：  \n",
    "\n",
    "格式转换工具：  \n",
    "qimage_to_pil()：将 Qt 的 QImage 格式转换为 PIL Image 格式，用于模型输入处理  \n",
    "摄像头控制：  \n",
    "toggle_camera()：切换摄像头开关状态  \n",
    "开启时启动定时器（30ms / 帧），更新摄像头画面并预测  \n",
    "关闭时停止定时器  \n",
    "实时帧处理与预测：  \n",
    "update_frame()：摄像头每帧回调函数  \n",
    "读取摄像头画面并转换为 RGB 格式  \n",
    "使用预训练模型进行实时预测  \n",
    "在画面上叠加预测结果文字  \n",
    "将处理后的画面转换为 Qt 格式并显示  \n",
    "更新界面上的预测结果标签  \n",
    "异常处理：  \n",
    "捕获并打印帧处理和预测过程中的异常信息  \n",
    "支持打印详细的堆栈跟踪  \n",
    "\n",
    "该模块通过定时器机制实现了摄像头画面的实时捕获、处理和显示，结合预训练模型实现了水果类别的实时识别，为用户提供了直观的交互体验。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "348eaf6b",
   "metadata": {},
   "outputs": [],
   "source": [
    "\n",
    "if __name__ == '__main__':\n",
    "    import traceback\n",
    "    try:\n",
    "        print(\"程序启动\")\n",
    "        app = QApplication(sys.argv)\n",
    "        print(\"创建 QApplication 成功\")\n",
    "        ex = FruitRecognitionApp()\n",
    "        print(\"创建窗口成功\")\n",
    "        ex.show()\n",
    "        print(\"窗口显示成功\")\n",
    "        sys.exit(app.exec_())\n",
    "    except Exception as e:\n",
    "        print(\"程序运行时发生异常：\", e)\n",
    "        traceback.print_exc()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "37ce8d74",
   "metadata": {},
   "source": [
    "这段代码是水果识别应用的主程序入口，主要功能包括：  \n",
    "\n",
    "异常处理机制：  \n",
    "使用try-except捕获程序启动过程中的异常  \n",
    "打印异常信息并输出详细堆栈跟踪（traceback.print_exc()）  \n",
    "应用初始化流程：  \n",
    "打印 \"程序启动\" 标志  \n",
    "创建QApplication实例（PyQt 应用的基础）  \n",
    "实例化FruitRecognitionApp主窗口类  \n",
    "显示主窗口界面  \n",
    "事件循环管理：  \n",
    "通过app.exec_()启动 Qt 事件循环  \n",
    "使用sys.exit()确保程序正常退出  \n",
    "日志输出：  \n",
    "在关键步骤（如创建应用、窗口实例、显示窗口）输出状态信息  \n",
    "便于调试时跟踪程序执行流程  \n",
    "\n",
    "整体逻辑：该段代码作为程序入口，负责初始化 PyQt 应用环境，创建并显示主窗口，同时通过异常处理机制确保程序在启动阶段的稳定性。当用户关闭窗口或程序发生异常时，事件循环终止并退出程序。\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6fd97681",
   "metadata": {},
   "source": [
    "整体总结：  \n",
    "总体实现了一个基于PyQt5和PyTorch的水果识别应用程序，涵盖了从图像处理、模型推理到用户界面设计的多个方面。通过模块化的设计和良好的异常处理，代码具有较高的可维护性和扩展性。    \n",
    "1. PyQt5 GUI开发  \n",
    "QApplication、QWidget、QLabel、QPushButton、QFileDialog、QVBoxLayout、QHBoxLayout：这些是PyQt5中用于构建图形用户界面的基本组件。代码通过继承QWidget创建了一个主窗口，并使用布局管理器（QVBoxLayout、QHBoxLayout）来组织界面元素。 和\n",
    "QTimer：用于定时器功能，控制摄像头的帧率更新。  \n",
    "QPixmap、QImage：用于在界面上显示图片和视频帧。  \n",
    "2. 深度学习模型的使用  \n",
    "torch、torch.nn、torchvision.models：代码使用了PyTorch框架，加载了一个预训练的VGG16模型，并对其进行了微调以适应特定的分类任务。  \n",
    "torchvision.transforms：用于对输入图像进行预处理，包括调整大小、转换为张量、归一化等操作。  \n",
    "model.load_state_dict：加载预训练的模型权重。  \n",
    "model.eval()：将模型设置为评估模式，关闭了训练时的特定行为（如Dropout）。  \n",
    "3. 图像处理  \n",
    "PIL.Image：用于加载和处理图像。  \n",
    "cv2.VideoCapture：用于从摄像头捕获视频帧。  \n",
    "cv2.cvtColor：将图像从BGR格式转换为RGB格式。  \n",
    "cv2.putText：在图像上绘制文本，显示预测结果。  \n",
    "4. 文件路径处理   \n",
    "os.path：用于处理文件路径，确保在不同操作系统下路径的正确性。  \n",
    "resource_path函数：用于获取打包后的资源路径，确保在打包后的应用程序中能够正确加载资源。  \n",
    "5. 异常处理  \n",
    "try-except块：在代码中广泛使用，用于捕获和处理可能出现的异常，确保程序的健壮性。  \n",
    "traceback.print_exc()：打印异常堆栈信息，便于调试。  \n",
    "6. 多线程与定时器  \n",
    "QTimer：用于定时更新摄像头捕获的帧，实现实时视频流的显示和预测。  \n",
    "7. 界面布局与交互  \n",
    "QVBoxLayout、QHBoxLayout：用于垂直和水平布局界面元素。  \n",
    "QPushButton的clicked信号：用于绑定按钮点击事件，实现用户交互功能，如上传图片、打开摄像头等。  \n",
    "8. 模型推理与预测  \n",
    "model(image_tensor)：将预处理后的图像张量输入模型进行推理。  \n",
    "torch.max(outputs, 1)：获取模型输出的最大概率类别，作为预测结果。  \n",
    "9. 代码结构与模块化  \n",
    "FruitRecognitionApp类：将整个应用程序的功能封装在一个类中，便于管理和扩展。  \n",
    "initUI方法：初始化界面布局和组件。  \n",
    "uploadImage、predictImage、toggle_camera等方法：分别处理图片上传、模型预测、摄像头开关等具体功能。  \n",
    "10. 日志与调试  \n",
    "print语句：在关键步骤输出日志信息，便于调试和跟踪程序执行流程  "
   ]
  }
 ],
 "metadata": {
  "language_info": {
   "name": "python"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
